import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import lightgbm as lgb
import holidays
from openai import OpenAI 
from transformers import AutoTokenizer, AutoModel
import torch
import gc
import warnings
import pickle
import json
import requests
from bs4 import BeautifulSoup, Tag
from feature_cache_manager import FeatureCacheManager
warnings.filterwarnings('ignore')

def get_fund_info_from_eastmoney(fund_code):
    """
    通过网页抓取从天天基金网获取基金背景信息和财务指标。
    """
    fund_info = {}
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
        'Referer': 'http://fund.eastmoney.com/'
    }
    
    # 1. 抓取基本信息
    try:
        url = f"http://fund.eastmoney.com/{fund_code}.html"
        print(f"🔍 正在访问URL: {url}")
        response = requests.get(url, headers=headers, timeout=15)
        response.raise_for_status()
        response.encoding = response.apparent_encoding
        print(f"✅ HTTP响应状态: {response.status_code}")
        
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # 根据实际HTML结构解析基本信息
        info_div = soup.find('div', class_='infoOfFund')
        if info_div and isinstance(info_div, Tag):
            print(f"🔍 开始解析基金基本信息...")
            # 解析表格中的所有文本，然后用正则或字符串匹配提取信息
            all_text = info_div.get_text(separator=' | ', strip=True)
            print(f"📋 提取的文本: {all_text}")
            
            # 用正则表达式提取关键信息
            import re
            
            # 提取基金类型 - 修复正则表达式以匹配实际格式
            type_match = re.search(r'类型：[|\s]*([^|]+?)[|\s]*中低风险', all_text)
            if not type_match:
                type_match = re.search(r'债券型-长债', all_text)
                if type_match:
                    fund_info['基金类型'] = '债券型-长债'
            else:
                fund_info['基金类型'] = type_match.group(1).strip()
            
            if fund_info.get('基金类型'):
                print(f"    ✅ 基金类型: {fund_info['基金类型']}")
            
            # 提取资产规模 - 修复正则表达式
            scale_match = re.search(r'规模[|\s]*：([^|]+?)（', all_text)
            if scale_match:
                fund_info['资产规模'] = scale_match.group(1).strip()
                print(f"    ✅ 资产规模: {fund_info['资产规模']}")
            
            # 提取管理人 - 修复正则表达式
            manager_match = re.search(r'管 理 人[|\s]*：[|\s]*([^|]+)', all_text)
            if manager_match:
                fund_info['管理人'] = manager_match.group(1).strip()
                print(f"    ✅ 管理人: {fund_info['管理人']}")
            
            # 提取成立日期 - 修复正则表达式
            date_match = re.search(r'成 立 日[|\s]*：([^|]+)', all_text)
            if date_match:
                fund_info['成立日期'] = date_match.group(1).strip()
                print(f"    ✅ 成立日期: {fund_info['成立日期']}")
        
        print(f"📋 基本信息解析结果: {fund_info}")
        
    except Exception as e:
        print(f"❌ 抓取基金 {fund_code} 基本信息失败: {e}")

    # 2. 抓取基金概况
    try:
        url_jbgk = f"http://fundf10.eastmoney.com/jbgk_{fund_code}.html"
        print(f"🔍 正在访问基金概况URL: {url_jbgk}")
        response = requests.get(url_jbgk, headers=headers, timeout=15)
        response.raise_for_status()
        response.encoding = response.apparent_encoding
        print(f"✅ 基金概况HTTP响应状态: {response.status_code}")
        
        soup_jbgk = BeautifulSoup(response.text, 'html.parser')
        
        # 根据调试日志，概况信息在table中，但是是连续文本格式
        table = soup_jbgk.find('table', class_='info w790')
        if table:
            print(f"✅ 找到概况表格，开始解析...")
            # 获取整个表格的文本
            table_text = table.get_text(separator=' | ', strip=True)
            print(f"📋 表格文本前500字符: {table_text[:500]}")
            
            import re
            
            # 提取基金全称 - 修复正则表达式以匹配管道符格式
            name_match = re.search(r'基金全称[|\s]*\|[|\s]*([^|]+?)[|\s]*\|', table_text)
            if name_match:
                fund_info['基金全称'] = name_match.group(1).strip()
                print(f"    ✅ 基金全称: {fund_info['基金全称']}")
            
            # 从概况表格中也可以提取基金类型，作为备用
            if not fund_info.get('基金类型'):
                type_match = re.search(r'基金类型[|\s]*\|[|\s]*([^|]+?)[|\s]*\|', table_text)
                if type_match:
                    fund_info['基金类型'] = type_match.group(1).strip()
                    print(f"    ✅ 基金类型（从概况获取）: {fund_info['基金类型']}")
            
            # 从概况表格中提取基金管理人，作为备用
            if not fund_info.get('管理人'):
                manager_match = re.search(r'基金管理人[|\s]*\|[|\s]*([^|]+?)[|\s]*\|', table_text)
                if manager_match:
                    fund_info['管理人'] = manager_match.group(1).strip()
                    print(f"    ✅ 管理人（从概况获取）: {fund_info['管理人']}")
        
        # 查找投资范围和投资策略等信息（在单独的div容器中）
        print(f"🔍 查找投资范围和投资策略信息...")
        
        # 查找所有的box容器
        boxes = soup_jbgk.find_all('div', class_='box')
        print(f"📋 找到{len(boxes)}个信息框")
        
        for i, box in enumerate(boxes):
            # 查找h4标题
            h4_title = box.find('h4', class_='t')
            if h4_title:
                title_text = h4_title.get_text(strip=True)
                print(f"    📋 信息框{i}标题: {title_text}")
                
                # 查找对应的内容段落
                content_p = box.find('p')
                if content_p:
                    content_text = content_p.get_text(strip=True)
                    
                    if '投资范围' in title_text:
                        fund_info['投资范围'] = content_text
                        print(f"    ✅ 投资范围: {content_text[:100]}...")
                    
                    elif '投资策略' in title_text:
                        fund_info['投资策略'] = content_text
                        print(f"    ✅ 投资策略: {content_text[:100]}...")
                    
                    elif '投资目标' in title_text:
                        fund_info['投资目标'] = content_text
                        print(f"    ✅ 投资目标: {content_text[:100]}...")
                    
                    elif '风险收益特征' in title_text:
                        fund_info['风险收益特征'] = content_text
                        print(f"    ✅ 风险收益特征: {content_text[:100]}...")
                    
                    elif '分红政策' in title_text:
                        fund_info['分红政策'] = content_text
                        print(f"    ✅ 分红政策: {content_text[:100]}...")
                    
                    elif '投资理念' in title_text:
                        fund_info['投资理念'] = content_text
                        print(f"    ✅ 投资理念: {content_text[:100]}...")
        
        # 寻找投资目标和投资范围 - 保留原有的表格行查找作为备用
        if not fund_info.get('投资目标') or not fund_info.get('投资范围'):
            print(f"🔍 在表格行中查找缺失的投资信息...")
            if table:
                rows = table.find_all('tr')
                for i, row in enumerate(rows):
                    row_text = row.get_text(strip=True)
                    if '投资目标' in row_text and not fund_info.get('投资目标'):
                        # 提取投资目标
                        target_match = re.search(r'投资目标(.+?)(?:投资范围|业绩比较|跟踪标的|$)', row_text)
                        if target_match:
                            fund_info['投资目标'] = target_match.group(1).strip()
                            print(f"    ✅ 投资目标（从表格获取）: {fund_info['投资目标'][:50]}...")
                    
                    if '投资范围' in row_text and not fund_info.get('投资范围'):
                        # 提取投资范围
                        scope_match = re.search(r'投资范围(.+?)(?:业绩比较|跟踪标的|$)', row_text)
                        if scope_match:
                            fund_info['投资范围'] = scope_match.group(1).strip()
                            print(f"    ✅ 投资范围（从表格获取）: {fund_info['投资范围'][:50]}...")
        
        print(f"📋 概况信息解析结果: {fund_info}")
                
    except Exception as e:
        print(f"❌ 抓取基金 {fund_code} 概况失败: {e}")

    # 3. 抓取财务指标
    financial_indicators = {}
    try:
        url_cwzb = f"http://fundf10.eastmoney.com/cwzb_{fund_code}.html"
        print(f"🔍 正在访问财务指标URL: {url_cwzb}")
        response = requests.get(url_cwzb, headers=headers, timeout=15)
        response.raise_for_status()
        response.encoding = response.apparent_encoding
        print(f"✅ 财务指标HTTP响应状态: {response.status_code}")
        
        soup_cwzb = BeautifulSoup(response.text, 'html.parser')
        
        print(f"🔎 查找财务指标数据...")
        
        # 由于财务数据是动态加载的，我们采用更简单的方法
        # 从基金概况页面提取一些基本的财务相关信息
        
        # 从之前获取的基金信息中提取财务相关数据
        if fund_info.get('资产规模'):
            financial_indicators['资产规模'] = fund_info['资产规模']
            print(f"    ✅ 资产规模: {financial_indicators['资产规模']}")
        
        # 从概况页面提取费率信息作为财务指标的补充
        if '管理费率' in str(soup_jbgk if 'soup_jbgk' in locals() else ''):
            # 尝试从概况信息中提取费率
            try:
                # 重新访问概况页面获取费率信息
                url_jbgk = f"http://fundf10.eastmoney.com/jbgk_{fund_code}.html"
                response_jbgk = requests.get(url_jbgk, headers=headers, timeout=10)
                response_jbgk.encoding = response_jbgk.apparent_encoding
                soup_jbgk_fees = BeautifulSoup(response_jbgk.text, 'html.parser')
                
                page_text = soup_jbgk_fees.get_text()
                
                import re
                
                # 提取管理费率
                mgmt_fee_match = re.search(r'管理费率[|\s]*([0-9.]+%)', page_text)
                if mgmt_fee_match:
                    financial_indicators['管理费率'] = mgmt_fee_match.group(1)
                    print(f"    ✅ 管理费率: {financial_indicators['管理费率']}")
                
                # 提取托管费率
                custody_fee_match = re.search(r'托管费率[|\s]*([0-9.]+%)', page_text)
                if custody_fee_match:
                    financial_indicators['托管费率'] = custody_fee_match.group(1)
                    print(f"    ✅ 托管费率: {financial_indicators['托管费率']}")
                
                # 提取成立来分红信息
                dividend_match = re.search(r'成立来分红[|\s]*每份累计([0-9.]+元)', page_text)
                if dividend_match:
                    financial_indicators['累计分红'] = dividend_match.group(1)
                    print(f"    ✅ 累计分红: {financial_indicators['累计分红']}")
                
            except Exception as e:
                print(f"    ⚠️ 提取费率信息时出错: {e}")
        
        # 尝试从当前财务页面提取一些基本信息
        page_text = soup_cwzb.get_text()
        
        # 查找页面中的数字，作为财务指标的参考
        import re
        
        # 查找大额数字（可能是财务数据）
        large_numbers = re.findall(r'([0-9,]+\.[0-9]{2,})', page_text)
        if large_numbers:
            # 过滤出合理范围的数字
            valid_numbers = []
            for num in large_numbers:
                try:
                    num_value = float(num.replace(',', ''))
                    if 1000 < num_value < 10000000000:  # 在合理的财务数据范围内
                        valid_numbers.append(num)
                except:
                    continue
            
            if valid_numbers:
                financial_indicators['财务数据样本'] = valid_numbers[:3]  # 取前3个作为样本
                print(f"    ✅ 财务数据样本: {valid_numbers[:3]}")
        
        # 如果还是没有找到数据，提供一个基本的财务状态描述
        if not financial_indicators:
            financial_indicators['状态'] = '财务数据需要动态加载，暂时无法直接获取'
            print(f"    ℹ️ 财务指标状态: {financial_indicators['状态']}")
        
        print(f"📋 财务指标解析结果: {financial_indicators}")
        
    except Exception as e:
        print(f"❌ 抓取基金 {fund_code} 财务指标失败: {e}")
        # 提供默认的财务指标信息
        financial_indicators = {
            '状态': '财务数据获取失败',
            '备注': '建议通过官方渠道查询最新财务数据'
        }

    print(f"🏁 最终fund_info: {fund_info}")
    
    # 构建返回结果（包含所有抓取的信息）
    result = {
        "基金全称": fund_info.get("基金全称", "N/A"),
        "基金类型": fund_info.get("基金类型", "N/A"),
        "资产规模": fund_info.get("资产规模", "N/A"),
        "基金公司": fund_info.get("管理人", "N/A"),
        "投资目标": fund_info.get("投资目标", "N/A"),
        "投资理念": fund_info.get("投资理念", "N/A"),
        "投资范围": fund_info.get("投资范围", "N/A"),
        "投资策略": fund_info.get("投资策略", "N/A"),
        "风险收益特征": fund_info.get("风险收益特征", "N/A"),
        "分红政策": fund_info.get("分红政策", "N/A"),
        "财务指标": financial_indicators
    }
    
    print(f"🎯 返回结果: {result}")
    return result

def call_qwen_api(prompt, model_name="qwen2.5-1.5b-instruct", temperature=0.7, max_retries=3, retry_delay=2):
    """调用通义千问 API 生成文本，带重试机制"""
    import time
    
    # 初始化通义千问 API 客户端
    client = OpenAI(
        api_key="sk-809e032f48a04d74a7335868809fd45b", 
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )
    
    for attempt in range(max_retries):
        try:
            completion = client.chat.completions.create(
                model=model_name,
                messages=[
                    {"role": "system", "content": "You are a helpful assistant."},
                    {"role": "user", "content": prompt},
                ],
                temperature=temperature,
                extra_body={"enable_thinking": False}
            )
            response_content = completion.choices[0].message.content
            return response_content.strip() if response_content else ""
            
        except Exception as e:
            print(f"API调用失败 (尝试 {attempt + 1}/{max_retries}): {e}")
            
            if attempt < max_retries - 1:
                # 指数退避策略：每次重试延迟时间翻倍
                delay = retry_delay * (2 ** attempt)
                print(f"等待 {delay} 秒后重试...")
                time.sleep(delay)
            else:
                print("所有重试都失败，返回空字符串")
                return ""
    
    return ""

def generate_llm_features(df, cache_path="llm_features.pkl", max_retries=5):
    """
    基于通义千问大模型API的基金特征生成函数
    - 仅使用基金代码作为输入
    - 多阶段提示工程生成丰富特征
    - 完整记录大模型交互过程
    """
    # 尝试加载缓存的特征
    if os.path.exists(cache_path):
        print(f"加载缓存的大模型特征: {cache_path}")
        with open(cache_path, 'rb') as f:
            cache_data = pickle.load(f)
            
            # 检查缓存格式
            if cache_path.endswith("llm_features_by_fund.pkl"):
                # 新格式：检查是否所有需要的基金都在缓存中
                cached_funds = set(cache_data.get('features', {}).keys())
                required_funds = set(df['fund_code'].unique())
                
                if required_funds.issubset(cached_funds):
                    print(f"✅ 缓存命中，所有 {len(required_funds)} 个基金都已缓存")
                    
                    # 构建特征DataFrame
                    features_data = []
                    for fund_code in required_funds:
                        feat = cache_data['features'][fund_code]
                        # 嵌入特征
                        embedding_features = {f"embed_{i}": val for i, val in enumerate(feat["embeddings"])}
                        
                        # 标签特征
                        labels = feat.get("labels", {})
                        label_features = {
                            "fund_type": labels.get("基金类型", "混合型"),
                            "risk_level": labels.get("风险等级", "中风险"),
                            "main_industry": ",".join(labels.get("主要投资行业", ["其他"])),
                            "investment_style": labels.get("投资风格", "平衡型"),
                            "liquidity": labels.get("流动性等级", "中"),
                            "asset_scale_level": labels.get("资产规模等级", "中型"),
                            "fee_level": labels.get("费用水平", "中费用"),
                            "dividend_feature": labels.get("分红特征", "中分红"),
                            "strategy_type": labels.get("投资策略类型", "其他"),
                            "company_strength": labels.get("基金公司实力", "中等公司")
                        }
                        
                        features_data.append({
                            "fund_code": fund_code,
                            **embedding_features,
                            **label_features,
                            "llm_description": feat.get("description", "")
                        })
                    
                    features_df = pd.DataFrame(features_data)
                    result_df = df.merge(features_df, on='fund_code', how='left')
                    del result_df['llm_description']
                    return result_df
                else:
                    missing_funds = required_funds - cached_funds
                    print(f"⚠️ 缓存不完整，缺少 {len(missing_funds)} 个基金: {list(missing_funds)[:5]}...")
            else:
                # 旧格式：验证缓存是否匹配当前数据
                if cache_data.get('fund_codes') == df['fund_code'].unique().tolist():
                    return df.merge(cache_data['features'], on='fund_code', how='left')
                
    # 存储所有大模型交互记录
    llm_records = []
    fund_features = {}
    
    print("使用通义千问大模型生成基金特征...")
    
    for fund_code in df['fund_code'].unique():
        print(f"处理基金: {fund_code}")
        record = {"fund_code": fund_code, "prompts": [], "responses": []}
        
        # 第一阶段：生成基金描述
        background_info_text = ""
        
        # 获取基金背景信息（带重试）
        print(f"🔍 获取基金 {fund_code} 的背景信息...")
        fund_background = None
        for info_attempt in range(3):  # 基金信息获取重试3次
            try:
                fund_background = get_fund_info_from_eastmoney(fund_code)
                if fund_background and fund_background.get('基金类型'):
                    print(f"✅ 基金信息获取成功")
                    break
                else:
                    print(f"⚠️ 基金信息不完整 (尝试 {info_attempt+1}/3)")
            except Exception as e:
                print(f"❌ 基金信息获取异常 (尝试 {info_attempt+1}/3): {e}")
            
            if info_attempt < 2:
                print("等待2秒后重试...")
                import time
                time.sleep(2)
        
        if fund_background:
            # 格式化财务指标文本，如果存在
            financial_text = "财务数据未获取到。"
            financials = fund_background.get("财务指标")
            if financials:
                financial_text = f"最新财报({financials.get('报告期', 'N/A')})显示净利润为{financials.get('净利润', 'N/A')}，总权益为{financials.get('所有者权益(或股东权益)合计', 'N/A')}。"
            
            background_info_text = f"""
        作为参考，这是从网络上抓取的该基金的背景信息：
        - 基金全称: {fund_background.get('基金全称', 'N/A')}
        - 基金类型: {fund_background.get('基金类型', 'N/A')}
        - 基金公司: {fund_background.get('基金公司', 'N/A')}
        - 资产规模: {fund_background.get('资产规模', 'N/A')}
        - 投资目标: {fund_background.get('投资目标', 'N/A')}
        - 投资理念: {fund_background.get('投资理念', 'N/A')}
        - 投资范围: {fund_background.get('投资范围', 'N/A')[:200] if fund_background.get('投资范围') else 'N/A'}...
        - 投资策略: {fund_background.get('投资策略', 'N/A')[:300] if fund_background.get('投资策略') else 'N/A'}...
        - 风险收益特征: {fund_background.get('风险收益特征', 'N/A')}
        - 分红政策: {fund_background.get('分红政策', 'N/A')[:150] if fund_background.get('分红政策') else 'N/A'}...
        - 财务状况: {financial_text}
        """

        desc_prompt = f"""
        你是一名专业的金融分析师，需要为基金代码 {fund_code} 创建一个详细描述。
        {background_info_text}
        请基于以上背景信息（特别是投资策略和财务状况）和基金代码结构生成合理的描述，包括：
        1. 对基金概况的总结 (投资目标、范围和财务表现)
        2. 主要投资方向和行业分布
        3. 风险收益特征（基于其策略和规模）
        4. 基金公司的背景和实力
        5. 典型投资策略和资产配置
        6. 基金的业绩表现
        7. 其他的关键信息
        
        描述要求：
        - 专业、客观、符合中国基金市场实际
        - 深入分析，而不仅仅是复述背景信息
        - 不少于150字
        
        输出格式：纯文本描述
        """
        
        record["prompts"].append(desc_prompt)
        fund_desc = None
        
        # 第一阶段重试：生成基金描述
        print(f"🔄 生成基金 {fund_code} 的描述...")
        for attempt in range(max_retries):
            try:
                fund_desc = call_qwen_api(desc_prompt, max_retries=2)  # API内部也有重试
                if fund_desc and len(fund_desc) > 100:
                    print(f"✅ 描述生成成功 (长度: {len(fund_desc)})")
                    break
                else:
                    print(f"⚠️ 描述过短或为空 (尝试 {attempt+1}/{max_retries})")
            except Exception as e:
                print(f"❌ 描述生成异常 (尝试 {attempt+1}/{max_retries}): {e}")
            
            if attempt < max_retries - 1:
                print(f"等待3秒后重试...")
                import time
                time.sleep(1)
        
        if not fund_desc or len(fund_desc) < 50:
            print(f"🔧 基金 {fund_code} 描述生成失败，使用默认描述")
            # 基于真实基金信息生成更好的默认描述
            fund_type = fund_background.get('基金类型', '混合型基金') if fund_background else '混合型基金'
            fund_company = fund_background.get('基金公司', '知名基金公司') if fund_background else '知名基金公司'
            fund_desc = f"这是一只{fund_type}，由{fund_company}管理，采用专业的投资策略，适合中等风险偏好的投资者。基金注重风险控制，追求长期稳定收益。"
        
        record["responses"].append(fund_desc)
        
        # 第二阶段：提取结构化标签（增强版 - 利用详细基金信息）
        # 复用第一阶段已获取的基金信息，避免重复调用
        fund_info = fund_background
        
        # 构建更丰富的提示信息
        detailed_info = f"""
        基金代码：{fund_code}
        基金全称：{fund_info.get('基金全称', 'N/A')}
        基金类型：{fund_info.get('基金类型', 'N/A')}
        基金公司：{fund_info.get('基金公司', 'N/A')}
        资产规模：{fund_info.get('资产规模', 'N/A')}
        
        投资目标：{fund_info.get('投资目标', 'N/A')}
        
        投资理念：{fund_info.get('投资理念', 'N/A')}
        
        投资范围：{fund_info.get('投资范围', 'N/A')}
        
        投资策略：{fund_info.get('投资策略', 'N/A')[:500] if fund_info.get('投资策略') else 'N/A'}...
        
        风险收益特征：{fund_info.get('风险收益特征', 'N/A')}
        
        分红政策：{fund_info.get('分红政策', 'N/A')[:200] if fund_info.get('分红政策') else 'N/A'}...
        
        财务指标：
        - 管理费率：{fund_info.get('财务指标', {}).get('管理费率', 'N/A')}
        - 托管费率：{fund_info.get('财务指标', {}).get('托管费率', 'N/A')}
        - 累计分红：{fund_info.get('财务指标', {}).get('累计分红', 'N/A')}
        
        基金描述：{fund_desc}
        """
        
        label_prompt = f"""
        根据以下详细的基金信息，提取结构化标签：
        
        {detailed_info}
        
        请基于真实的基金信息提取以下字段：
        1. 基金类型（从以下选择：股票型、债券型、混合型、货币型、QDII、指数型、行业主题型、量化对冲型）
        2. 风险等级（从以下选择：低风险、中低风险、中风险、中高风险、高风险）
        3. 主要投资行业（最多3个，从以下选择：科技、金融、消费、医疗、能源、工业、房地产、原材料、公用事业、政府债券、企业债券、其他）
        4. 投资风格（从以下选择：成长型、价值型、平衡型、行业轮动型、稳健型、积极型）
        5. 流动性等级（高、中、低）
        6. 资产规模等级（根据资产规模分类：大型(>50亿)、中型(10-50亿)、小型(<10亿)）
        7. 费用水平（根据管理费率分类：低费用(<0.5%)、中费用(0.5%-1.5%)、高费用(>1.5%)）
        8. 分红特征（根据累计分红情况：高分红(>0.5元)、中分红(0.1-0.5元)、低分红(<0.1元)、无分红）
        9. 投资策略类型（从投资范围和目标中提取：纯债策略、信用债策略、可转债策略、多元化债券策略、其他）
        10. 基金公司实力（根据基金公司知名度：头部公司、中等公司、小型公司）
        
        输出格式：JSON对象，包含上述10个字段
        
        分析要点：
        - 优先使用真实的基金类型信息
        - 根据投资范围和投资策略判断主要投资行业和策略类型
        - 根据资产规模数值确定规模等级
        - 根据管理费率数值确定费用水平
        - 根据累计分红数值和分红政策确定分红特征
        - 根据风险收益特征描述确定风险等级
        - 根据投资策略内容确定投资风格（如稳健型、积极型等）
        - 债券型基金通常为低-中低风险，但要结合具体的风险收益特征描述
        """
        
        record["prompts"].append(label_prompt)
        labels_json = None
        labels_str = None  # 保存原始响应
        parse_failed_count = 0  # 解析失败计数
        
        # 第二阶段重试：提取结构化标签
        print(f"🔄 提取基金 {fund_code} 的结构化标签...")
        for attempt in range(2):
            try:
                labels_str = call_qwen_api(label_prompt, temperature=0.3, max_retries=2)
                if labels_str:
                    # 智能提取JSON内容
                    def extract_json_from_response(text):
                        """从LLM响应中智能提取JSON内容"""
                        # 移除markdown标记
                        text = text.replace('```json', '').replace('```', '')
                        
                        # 方法1: 找到第一个{和最后一个}，提取JSON对象
                        first_brace = text.find('{')
                        last_brace = text.rfind('}')
                        
                        if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
                            json_str = text[first_brace:last_brace+1].strip()
                            return json_str
                        
                        # 方法2: 如果没有找到大括号，返回清理后的文本
                        return text.strip()
                    
                    clean_labels_str = extract_json_from_response(labels_str)
                    
                    # 尝试解析JSON
                    try:
                        labels_json = json.loads(clean_labels_str)
                        print(f"✅ 标签提取成功，包含 {len(labels_json)} 个字段")
                        break
                    except json.JSONDecodeError as je:
                        parse_failed_count += 1
                        print(f"⚠️ JSON解析失败 (尝试 {attempt+1}/2): {je}")
                        print(f"原始响应: {labels_str[:200]}...")
                        print(f"清理后内容: {clean_labels_str[:200]}...")
                        # 保存原始响应以备后用
                        record["responses"].append(labels_str)
                else:
                    print(f"⚠️ 标签响应为空 (尝试 {attempt+1}/2)")
            except Exception as e:
                print(f"❌ 标签提取异常 (尝试 {attempt+1}/2): {e}")
            
            if attempt < 1:  # 只有一次重试机会
                print(f"等待3秒后重试...")
                import time
                time.sleep(3)
        
        # 处理解析失败的情况
        if not labels_json:
            if parse_failed_count >= 2 and labels_str:
                # 解析失败超过2次，但有原始响应，直接使用原始响应生成embedding
                print(f"🔄 基金 {fund_code} 结构化解析失败 {parse_failed_count} 次，将使用原始响应生成embedding")
                print(f"📝 原始响应内容: {labels_str[:300]}...")
                
                # 将原始响应追加到基金描述中，用于生成更丰富的embedding
                enhanced_desc = f"{fund_desc}\n\n标签分析结果:\n{labels_str}"
                
                # 使用增强的描述替换原始描述
                fund_desc = enhanced_desc
                print(f"✅ 已将原始响应融合到描述中，用于embedding生成")
                
                # 仍然需要提供默认的结构化标签用于特征工程
                fund_type = fund_info.get('基金类型', '混合型')
                if '债券' in fund_type:
                    default_risk = "中低风险"
                    default_industry = ["政府债券", "企业债券"]
                    default_style = "稳健型"
                    default_strategy = "多元化债券策略"
                else:
                    default_risk = "中风险"
                    default_industry = ["其他"]
                    default_style = "平衡型"
                    default_strategy = "其他"
                
                labels_json = {
                    "基金类型": fund_type,
                    "风险等级": default_risk,
                    "主要投资行业": default_industry,
                    "投资风格": default_style,
                    "流动性等级": "中",
                    "资产规模等级": "中型",
                    "费用水平": "中费用",
                    "分红特征": "中分红",
                    "投资策略类型": default_strategy,
                    "基金公司实力": "中等公司"
                }
                print(f"📊 使用默认结构化标签用于特征工程")
                
            else:
                # 没有有效的原始响应，说明LLM调用失败，跳过这个基金
                print(f"❌ 基金 {fund_code} LLM调用失败，无有效响应，跳过该基金的LLM特征生成")
                print(f"⚠️ 该基金将只使用基础特征和市场特征，不包含LLM特征")
                # continue  # 跳过当前基金，继续处理下一个
        
        # 保存最终的结构化标签
        record["responses"].append(labels_json)
        
        # 第三阶段：生成嵌入向量（带重试机制）
        print(f"🔄 生成基金 {fund_code} 的嵌入向量...")
        embeddings = None
        
        for embed_attempt in range(3):  # 嵌入生成重试3次
            try:
                print(f"   尝试加载预训练模型 (尝试 {embed_attempt+1}/3)...")
                
                # 加载tokenizer和模型
                tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-small-zh-v1.5", 
                                                         trust_remote_code=True,
                                                         resume_download=True)
                model = AutoModel.from_pretrained("BAAI/bge-small-zh-v1.5", 
                                                 trust_remote_code=True,
                                                 resume_download=True)
                
                print(f"   ✅ 模型加载成功，开始生成嵌入...")
                
                # 生成嵌入
                inputs = tokenizer(fund_desc, return_tensors="pt", padding=True, truncation=True, max_length=256)
                with torch.no_grad():
                    outputs = model(**inputs)
                    embeddings = outputs.last_hidden_state.mean(dim=1).numpy()[0]
                
                print(f"   ✅ 嵌入向量生成成功 (维度: {len(embeddings)})")
                
                # 释放资源
                del tokenizer, model, inputs, outputs
                torch.cuda.empty_cache()
                gc.collect()
                
                break  # 成功生成，跳出重试循环
                
            except ImportError as ie:
                print(f"   ❌ 模型依赖缺失 (尝试 {embed_attempt+1}/3): {ie}")
                if embed_attempt < 2:
                    print("   等待2秒后重试...")
                    import time
                    time.sleep(2)
            except RuntimeError as re:
                print(f"   ❌ 模型运行时错误 (尝试 {embed_attempt+1}/3): {re}")
                if embed_attempt < 2:
                    print("   等待3秒后重试...")
                    import time
                    time.sleep(3)
            except Exception as e:
                print(f"   ❌ 嵌入生成异常 (尝试 {embed_attempt+1}/3): {e}")
                if embed_attempt < 2:
                    print("   等待2秒后重试...")
                    import time
                    time.sleep(2)
        
        # 如果所有重试都失败，使用随机嵌入
        if embeddings is None:
            print(f"   🔧 所有嵌入生成尝试都失败，使用随机嵌入向量")
            embeddings = np.random.randn(512)
            print(f"   📊 随机嵌入向量维度: {len(embeddings)}")
        
        # 验证嵌入向量质量
        if embeddings is not None:
            # 检查嵌入向量是否合理
            if len(embeddings) < 100:
                print(f"   ⚠️ 嵌入向量维度过小 ({len(embeddings)})，补充到512维")
                embeddings = np.pad(embeddings, (0, 512 - len(embeddings)), 'constant')
            elif len(embeddings) > 1000:
                print(f"   ⚠️ 嵌入向量维度过大 ({len(embeddings)})，截取到512维")
                embeddings = embeddings[:512]
            
            # 检查是否包含异常值
            if np.any(np.isnan(embeddings)) or np.any(np.isinf(embeddings)):
                print(f"   ⚠️ 嵌入向量包含异常值，重新生成随机向量")
                embeddings = np.random.randn(512)
            
            print(f"   ✅ 最终嵌入向量验证通过 (维度: {len(embeddings)}, 范围: [{embeddings.min():.3f}, {embeddings.max():.3f}])")
        
        # 存储特征
        fund_features[fund_code] = {
            "description": fund_desc,
            "labels": labels_json,
            "embeddings": embeddings
        }
        llm_records.append(record)
    
    # 构建特征DataFrame
    features_data = []
    for fund_code, feat in fund_features.items():
        # 嵌入特征
        embedding_features = {f"embed_{i}": val for i, val in enumerate(feat["embeddings"])}
        
        # 标签特征（增强版）
        label_features = {
            "fund_type": feat["labels"].get("基金类型", "混合型"),
            "risk_level": feat["labels"].get("风险等级", "中风险"),
            "main_industry": ",".join(feat["labels"].get("主要投资行业", ["其他"])),
            "investment_style": feat["labels"].get("投资风格", "平衡型"),
            "liquidity": feat["labels"].get("流动性等级", "中"),
            "asset_scale_level": feat["labels"].get("资产规模等级", "中型"),
            "fee_level": feat["labels"].get("费用水平", "中费用"),
            "dividend_feature": feat["labels"].get("分红特征", "中分红"),
            "strategy_type": feat["labels"].get("投资策略类型", "其他"),
            "company_strength": feat["labels"].get("基金公司实力", "中等公司")
        }
        
        features_data.append({
            "fund_code": fund_code,
            **embedding_features,
            **label_features,
            "llm_description": feat["description"]
        })
    
    features_df = pd.DataFrame(features_data)
    
    # 保存特征和交互记录
    # 检查是否使用新的缓存格式
    if cache_path.endswith("llm_features_by_fund.pkl"):
        # 新格式：按基金代码组织特征
        new_cache_data = {
            'features': {},
            'timestamp': datetime.now().isoformat(),
            'fund_count': len(fund_features),
            'last_update': datetime.now().isoformat()
        }
        
        # 为每个基金构建特征数据
        for fund_code, feat in fund_features.items():
            new_cache_data['features'][fund_code] = {
                'embeddings': feat["embeddings"],
                'labels': feat["labels"],
                'fund_code': fund_code,
                'feature_dim': len(feat["embeddings"]),
                'timestamp': datetime.now().isoformat(),
                'description': feat["description"]
            }
        
        with open(cache_path, 'wb') as f:
            pickle.dump(new_cache_data, f)
        print(f"大模型特征已缓存至: {cache_path} (新格式)")
    else:
        # 旧格式：保持兼容性
        cache_data = {
            "fund_codes": df['fund_code'].unique().tolist(),
            "features": features_df,
            "llm_records": llm_records,
            "timestamp": datetime.now().isoformat()
        }
        
        with open(cache_path, 'wb') as f:
            pickle.dump(cache_data, f)
        print(f"大模型特征已缓存至: {cache_path} (旧格式)")
    
    # 合并特征到原始DataFrame
    df = df.merge(features_df, on='fund_code', how='left')
    del df['llm_description']
    
    return df

# 时间特征 (13个)：年、月、日、星期、年内第几天、年内第几周、是否周末、是否月初/月末、是否季初/季末、是否节假日、日期的正弦/余弦周期特征
# 市场指数特征 (7个)：上证指数、深证成指、沪深300的价格和涨跌幅，以及是否交易日标识
# 基金净值特征 (3个)：基金净值、基金涨跌幅、累计净值
def add_basic_features(df):
    """
    添加基础时间工程
    """
    # 中国节假日
    years = df['transaction_date'].dt.year.unique()
    cn_holidays = holidays.country_holidays('CN', years=years)
    
    # 时间特征
    df['year'] = df['transaction_date'].dt.year
    df['month'] = df['transaction_date'].dt.month
    df['day'] = df['transaction_date'].dt.day
    df['day_of_week'] = df['transaction_date'].dt.dayofweek
    df['day_of_year'] = df['transaction_date'].dt.dayofyear
    df['week_of_year'] = df['transaction_date'].dt.isocalendar().week
    df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
    df['is_month_end'] = df['transaction_date'].dt.is_month_end.astype(int)
    df['is_month_start'] = df['transaction_date'].dt.is_month_start.astype(int)
    df['is_quarter_end'] = df['transaction_date'].dt.is_quarter_end.astype(int)
    df['is_quarter_start'] = df['transaction_date'].dt.is_quarter_start.astype(int)
    df['is_holiday'] = df['transaction_date'].apply(lambda x: x in cn_holidays).astype(int)
    
    # 周期性特征
    df['day_sin'] = np.sin(2 * np.pi * df['day_of_year'] / 365)
    df['day_cos'] = np.cos(2 * np.pi * df['day_of_year'] / 365)
    df['week_sin'] = np.sin(2 * np.pi * df['week_of_year'] / 52)
    df['week_cos'] = np.cos(2 * np.pi * df['week_of_year'] / 52)

    # 添加市场指数和基金净值特征（使用缓存机制）
    from func import add_market_features
    df = add_market_features(df, cache_path="market_features_cache.pkl")
    
    return df

def train_and_validate(df, last_train_date, last_validation_date):
    """
    训练模型并在验证集上验证
    返回最佳迭代次数和特征列
    """
    # 准备训练和验证数据
    train_df = df[df['transaction_date'] <= last_train_date].copy()
    validation_df = df[(df['transaction_date'] > last_train_date) & (df['transaction_date'] <= last_validation_date)].copy()

    # 特征列 - 只选择数值特征
    exclude_cols = ['fund_code','transaction_date','apply_amt','redeem_amt',
                   'uv_key_page_1','uv_key_page_2','uv_key_page_3']
    # 选择数值类型的列，排除目标变量和非特征列
    numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
    feature_cols = [col for col in numeric_cols if col not in exclude_cols]
    
    X_train = train_df[feature_cols]
    X_val = validation_df[feature_cols]
    
    # 训练申购模型
    print("训练申购模型...")
    y_train_apply = train_df['apply_amt']
    y_val_apply = validation_df['apply_amt']
    
    apply_model = lgb.LGBMRegressor(
        n_estimators=1000,
        learning_rate=0.05,
        num_leaves=31,
        min_child_samples=20,
        subsample=0.8,
        colsample_bytree=0.8,
        random_state=42
    )
    
    apply_model.fit(
        X_train, y_train_apply,
        eval_set=[(X_val, y_val_apply)],
        eval_metric='mape',
        callbacks=[lgb.log_evaluation(200), lgb.early_stopping(200)]
    )
    
    # 训练赎回模型
    print("训练赎回模型...")
    y_train_redeem = train_df['redeem_amt']
    y_val_redeem = validation_df['redeem_amt']
    
    redeem_model = lgb.LGBMRegressor(
        n_estimators=1000,
        learning_rate=0.05,
        num_leaves=31,
        min_child_samples=20,
        subsample=0.8,
        colsample_bytree=0.8,
        random_state=42
    )
    
    redeem_model.fit(
        X_train, y_train_redeem,
        eval_set=[(X_val, y_val_redeem)],
        eval_metric='mape',
        callbacks=[lgb.log_evaluation(200), lgb.early_stopping(200)]
    )
    
    # 获取最佳迭代次数
    best_apply_iter = apply_model.best_iteration_
    best_redeem_iter = redeem_model.best_iteration_
    
    return best_apply_iter, best_redeem_iter, feature_cols

def train_full_model(df, best_apply_iter, best_redeem_iter, feature_cols):
    """使用全量数据和最佳迭代次数训练模型"""
    # 训练申购模型
    print("全量训练申购模型...")
    X_full = df[feature_cols]
    y_apply_full = df['apply_amt']
    
    apply_model = lgb.LGBMRegressor(
        n_estimators=best_apply_iter,
        learning_rate=0.05,
        num_leaves=31,
        min_child_samples=20,
        subsample=0.8,
        colsample_bytree=0.8,
        random_state=42
    )
    apply_model.fit(X_full, y_apply_full)
    
    # 训练赎回模型
    print("全量训练赎回模型...")
    y_redeem_full = df['redeem_amt']
    
    redeem_model = lgb.LGBMRegressor(
        n_estimators=best_redeem_iter,
        learning_rate=0.05,
        num_leaves=31,
        min_child_samples=20,
        subsample=0.8,
        colsample_bytree=0.8,
        random_state=42
    )
    redeem_model.fit(X_full, y_redeem_full)
    
    return apply_model, redeem_model

def rolling_forecast(df, apply_model, redeem_model, feature_cols, last_date, forecast_days=7):
    """滚动预测未来多天，每天预测后更新数据并重新构建特征"""
    # 复制数据以避免修改原始数据
    init_cols = ['fund_code','transaction_date','apply_amt','redeem_amt','uv_key_page_1','uv_key_page_2','uv_key_page_3']
    forecast_df = df[init_cols].copy()

    llm_cols = [f for f in feature_cols if 'embed_' in f]
    llm_df = df[['fund_code']+llm_cols].drop_duplicates(subset=['fund_code'])
    
    # 存储预测结果
    all_preds = []
    
    # 生成预测日期
    current_date = last_date
    for day in range(1, forecast_days + 1):
        next_date = current_date + timedelta(days=day)
        print(f"预测日期: {next_date.strftime('%Y-%m-%d')}")
        
        # 创建预测日数据框架
        pred_day_df = pd.DataFrame()
        for fund_code in forecast_df['fund_code'].unique():
            # 获取该基金的最后一条数据
            fund_data = forecast_df[forecast_df['fund_code'] == fund_code].iloc[-1:].copy()
            # 更新日期
            fund_data['transaction_date'] = next_date
            # 重置索引以避免索引冲突
            fund_data = fund_data.reset_index(drop=True)
            # 添加预测日数据
            pred_day_df = pd.concat([pred_day_df, fund_data], axis=0, ignore_index=True)
        
        # 添加基础特征（标记为预测日）
        temp_df = pd.concat([forecast_df, pred_day_df], axis=0, ignore_index=True)
        temp_df = add_basic_features(temp_df)
        loc_pred_df = temp_df[temp_df['transaction_date'] == next_date].copy()

        # 合并llm特征
        loc_pred_df = loc_pred_df.merge(llm_df, on=['fund_code'], how='left')
        
        # 确保数据排序一致
        loc_pred_df = loc_pred_df.sort_values('fund_code').reset_index(drop=True)
        pred_day_df = pred_day_df.sort_values('fund_code').reset_index(drop=True)
        
        # 预测
        apply_pred = apply_model.predict(loc_pred_df[feature_cols])
        redeem_pred = redeem_model.predict(loc_pred_df[feature_cols])

        # 处理负值
        apply_pred = np.clip(apply_pred, 0, None)
        redeem_pred = np.clip(redeem_pred, 0, None)
        
        # 确保长度匹配后再赋值
        if len(apply_pred) == len(pred_day_df):
            pred_day_df.loc[:, 'apply_amt'] = apply_pred
            pred_day_df.loc[:, 'redeem_amt'] = redeem_pred
        else:
            print(f"警告: 预测结果长度({len(apply_pred)})与数据框长度({len(pred_day_df)})不匹配")
            # 如果长度不匹配，使用默认值
            pred_day_df.loc[:, 'apply_amt'] = 0
            pred_day_df.loc[:, 'redeem_amt'] = 0

        # 将预测日数据添加到历史数据中（用于后续预测）
        forecast_df = pd.concat([forecast_df, pred_day_df[init_cols]], axis=0, ignore_index=True)
        
        # 保存预测结果
        for i in range(len(pred_day_df)):
            fund_pred = {
                'fund_code': pred_day_df['fund_code'].iloc[i],
                'transaction_date': next_date.strftime('%Y%m%d'),
                'apply_amt_pred': pred_day_df['apply_amt'].iloc[i] if len(apply_pred) == len(pred_day_df) else apply_pred[i] if i < len(apply_pred) else 0,
                'redeem_amt_pred': pred_day_df['redeem_amt'].iloc[i] if len(redeem_pred) == len(pred_day_df) else redeem_pred[i] if i < len(redeem_pred) else 0
            }
            all_preds.append(fund_pred)
        
    return pd.DataFrame(all_preds)

def main():
    # 1. 初始化特征缓存管理器
    print("🚀 初始化特征缓存管理系统...")
    cache_manager = FeatureCacheManager()
    
    # 显示当前缓存状态
    print("\n📊 当前缓存状态:")
    stats = cache_manager.get_statistics()
    for cache_type, info in stats.items():
        print(f"  {cache_type}: {info}")
    
    # 2. 检测新增数据
    print("\n🔍 检测新增数据...")
    new_data_df, new_fund_codes = cache_manager.detect_new_data()
    
    if len(new_data_df) == 0:
        print("✅ 没有新增数据，直接使用缓存")
        # 加载完整的特征数据
        df = cache_manager.merge_features()
    else:
        print(f"📈 发现新增数据: {len(new_data_df)} 行，{len(new_fund_codes)} 个新基金")
        
        # 3. 处理新增数据
        print("\n🔧 处理新增数据...")
        
        # 添加基础特征到新增数据
        print("开始拼接市场特征到数据..")
        from func import add_market_features
        new_data_df = add_market_features(new_data_df, 
                                        cache_path="market_features_cache.pkl", 
                                        force_refresh=False)
        
        # 添加基础时间特征
        print("添加基础特征...")
        new_data_df = add_basic_features(new_data_df)
        
        # 4. 处理LLM特征
        print("\n🤖 处理LLM特征...")
        if len(new_fund_codes) > 0:
            print(f"为 {len(new_fund_codes)} 个新基金生成LLM特征")
            # 只为新基金生成LLM特征
            new_funds_df = new_data_df[new_data_df['fund_code'].isin(new_fund_codes)].copy()
            new_funds_df = generate_llm_features(new_funds_df, cache_path="llm_features.pkl")
            
            # 提取LLM特征字典
            llm_features_dict = {}
            if os.path.exists("llm_features.pkl"):
                with open("llm_features.pkl", 'rb') as f:
                    llm_data = pickle.load(f)
                    llm_features_dict = llm_data.get('features', {})
            
            # 更新LLM特征缓存
            cache_manager.update_llm_features(new_fund_codes, llm_features_dict)
        
        # 5. 更新增强特征缓存
        print("\n💾 更新增强特征缓存...")
        cache_manager.update_enhanced_features(new_data_df)
        
        # 6. 加载完整的特征数据
        print("\n📖 加载完整特征数据...")
        df = cache_manager.merge_features()
    
    # 确保数据格式正确
    if 'transaction_date' in df.columns and df['transaction_date'].dtype == 'object':
        df['transaction_date'] = pd.to_datetime(df['transaction_date'], format='%Y%m%d')
    df.sort_values(['fund_code', 'transaction_date'], inplace=True)
    
    print(f"✅ 特征数据准备完成: {df.shape}")
    print(f"📊 数据列数: {len(df.columns)}")
    print(f"📊 基金数量: {df['fund_code'].nunique()}")
    print(f"📊 日期范围: {df['transaction_date'].min()} 到 {df['transaction_date'].max()}")
    
    # 7. 设置日期
    last_train_date = datetime(2025, 6, 22)  # 最后训练日(最后一周前)
    last_validation_date = datetime(2025, 6, 28)  # 最后验证日(最后一周)
    last_known_date = datetime(2025, 6, 28)  # 最后已知数据日期
    
    # 8. 训练验证模型
    print("\n🎯 训练验证模型...")
    best_apply_iter, best_redeem_iter, feature_cols = train_and_validate(df, last_train_date, last_validation_date)
    
    # 9. 全量训练模型
    print("\n🏋️ 全量训练模型...")
    apply_model, redeem_model = train_full_model(df, best_apply_iter, best_redeem_iter, feature_cols)
    
    # 10. 滚动预测未来7天
    print("\n🔮 滚动预测未来7天...")
    predictions = rolling_forecast(df, apply_model, redeem_model, feature_cols, last_known_date, 7)
    
    # 11. 保存预测结果
    predictions.to_csv('predict_result.csv', index=False)
    print("✅ 预测结果已保存至 predict_result.csv")
    
    # 12. 显示最终缓存统计
    print("\n📊 最终缓存统计:")
    final_stats = cache_manager.get_statistics()
    for cache_type, info in final_stats.items():
        print(f"  {cache_type}:")
        for key, value in info.items():
            print(f"    {key}: {value}")

if __name__ == '__main__':
    main()